# Snow CLI User Guide - Custom Headers Plugin

## Overview

Snow CLI's custom headers feature lets you attach extra HTTP headers to API
requests. Starting from this version, header **values** support `{{placeholder}}`
syntax that can be dynamically resolved by plugins on every API request.

This enables you to:

- Reference dynamically generated OAuth tokens without manual updates
- Inject timestamps, request IDs, and other runtime variables
- Pull credentials from secret managers (e.g. Vault, AWS Secrets Manager)
- Inject different values based on the current environment (dev/prod)

> If no plugins are installed, custom headers behave exactly as before —
> `{{placeholders}}` are sent as-is with zero overhead.

## Plugin Directory

Snow CLI loads custom header plugins from:

```bash
~/.snow/plugin/custom_headers/
```

Supported file extensions:

- `.js`
- `.mjs` (recommended for plain ES Modules)
- `.cjs`

Notes:

- Plugins are loaded from the user directory only.
- Snow CLI sorts plugin files by filename and lazily loads them on the first
  API request whose headers contain `{{placeholders}}`.
- Adding, modifying, or deleting plugin files hot-reloads automatically —
  no Snow CLI restart required.
- If no header values contain `{{placeholders}}`, plugins are never loaded and
  behavior is identical to before.

## Export Formats

A plugin module can export in any of these forms (the loader scans all of them):

```js
export default { ... }
```

```js
export const customHeaderPlugin = { ... }
```

```js
export const customHeaderPlugins = [{ ... }, { ... }]
```

If multiple plugins can resolve the same placeholder, the plugin loaded first
(alphabetically by filename) wins (first-wins).

## Plugin Structure

Every plugin must satisfy this shape (TypeScript-style for clarity, but plugin
files are plain JavaScript):

```ts
interface CustomHeaderPlugin {
	id: string; // unique identifier
	name?: string; // optional, human-readable (used in logs)
	enable?: boolean; // optional, defaults to true
	resolve(
		placeholders: string[], // all placeholder names found in the active header scheme
		context: CustomHeaderPluginContext, // runtime context
	): Promise<Record<string, string>>; // returns a map of placeholder name → resolved value
}

interface CustomHeaderPluginContext {
	cwd: string; // current working directory
	platform: string; // OS platform (e.g. 'darwin', 'win32', 'linux')
	sessionId?: string; // current session ID (undefined for requests not tied to a session)
}
```

Field description:

- `id`: unique plugin identifier, used in logs. Keep it stable once published.
- `name` (optional): human-readable name, used only for logging.
- `enable` (optional): defaults to `true`. Set to `false` to temporarily disable
  a plugin without deleting its file.
- `resolve(placeholders, context)`: the core resolution function.
  - `placeholders`: all unique placeholder names extracted from the active
    header scheme's values. For example, a header value of `Bearer {{token}}`
    yields `["token"]`.
  - `context`: runtime context containing `cwd`, `platform`, and `sessionId` (current session ID; undefined for requests not tied to a conversation session, e.g. fetching the model list). The context is only passed to plugins and is never written into request headers automatically — whether to use it is up to your plugin.
  - Return value: a `Record<string, string>` mapping placeholder names to
    resolved strings. **Only include placeholders this plugin successfully
    resolved** — omit any it cannot resolve; they may be handled by another
    plugin or left as-is.

## Placeholder Syntax

Use `{{placeholder_name}}` syntax in custom header **values**:

| Header Key      | Header Value           | Extracted Placeholders              |
| --------------- | ---------------------- | ----------------------------------- |
| `Authorization` | `Bearer {{token}}`     | `["token"]`                         |
| `X-Timestamp`   | `{{timestamp}}`        | `["timestamp"]`                     |
| `X-Request-ID`  | `req-{{uuid}}-{{env}}` | `["uuid", "env"]`                   |
| `X-API-Key`     | `sk-abc123`            | `[]` (none — plugins not triggered) |

Rules:

- Placeholder names are automatically `trim()`-ed, so `{{ token }}` and
  `{{token}}` are equivalent.
- A single value can contain multiple placeholders.
- When the same placeholder name appears in multiple headers, it is resolved
  once and the result is reused everywhere.
- Placeholders not resolved by any plugin are left as-is (e.g. `{{token}}` is
  sent literally).

## Resolution Flow

```
Header values contain {{placeholders}}?
├── No → return original headers directly (zero overhead, no plugin loading)
└── Yes
    ├── Load plugins (lazy, first time only)
    ├── Any plugins loaded?
    │   ├── No → return original headers (placeholders left as-is)
    │   └── Yes
    │       ├── Call each plugin's resolve() in order
    │       ├── First plugin to resolve a placeholder wins (first-wins)
    │       ├── At least one placeholder resolved?
    │       │   ├── No → return original headers
    │       │   └── Yes → substitute placeholders, return new headers
    │       └── Plugin resolve() throws → log warning, continue to next plugin
    └──
```

## Lifecycle and Configuration

1. Configure a custom header scheme in Snow CLI's settings, using `{{placeholder}}`
   syntax in values.
2. Drop the plugin file under `~/.snow/plugin/custom_headers/`.
3. Start (or restart) Snow CLI.
4. All subsequent API requests (chat / anthropic / gemini / responses / models)
   will automatically resolve placeholders before sending.

> Placeholder resolution happens before every API request is sent. If your
> plugin's `resolve()` involves network calls (e.g. fetching an OAuth token),
> consider caching inside the plugin to avoid a network call on every request.

## Example: Timestamp & Environment Plugin

Below is a complete example plugin that resolves the `{{timestamp}}`, `{{env}}`,
`{{platform}}`, and `{{session_id}}` placeholders:

```js
// ~/.snow/plugin/custom_headers/timestamp-env.mjs

export default {
	id: 'timestamp-env',
	name: 'Timestamp & Environment Resolver',
	enable: true,

	async resolve(placeholders, context) {
		const result = {};

		for (const name of placeholders) {
			switch (name) {
				case 'timestamp':
					// ISO 8601 current timestamp
					result.timestamp = new Date().toISOString();
					break;

				case 'env':
					// Infer environment from working directory
					if (context.cwd.includes('/production/')) {
						result.env = 'prod';
					} else if (context.cwd.includes('/staging/')) {
						result.env = 'staging';
					} else {
						result.env = process.env.NODE_ENV || 'dev';
					}
					break;

				case 'platform':
					result.platform = context.platform;
					break;

				case 'session_id':
					// Current session ID (undefined for requests not tied to a session; omit it then)
					if (context.sessionId) {
						result.session_id = context.sessionId;
					}
					break;
			}
		}

		return result;
	},
};
```

Configure custom headers:

| Key              | Value            |
| ---------------- | ---------------- |
| `X-Request-Time` | `{{timestamp}}`  |
| `X-Environment`  | `{{env}}`        |
| `X-Platform`     | `{{platform}}`   |
| `X-Session-Id`   | `{{session_id}}` |

When the request is sent, these values are automatically replaced with actual
content.

## Example: OAuth Token with Caching

Below is a more practical example that fetches an OAuth access token and caches
it to avoid re-fetching on every request:

```js
// ~/.snow/plugin/custom_headers/oauth-token.mjs

// Simple in-memory cache
let cachedToken = null;
let cachedAt = 0;
const CACHE_TTL_MS = 50 * 60 * 1000; // 50 minutes (tokens usually expire in 1 hour)

async function fetchOAuthToken() {
	const now = Date.now();

	// Return cached token if still valid
	if (cachedToken && now - cachedAt < CACHE_TTL_MS) {
		return cachedToken;
	}

	// Read OAuth config from environment variables
	const tokenUrl = process.env.OAUTH_TOKEN_URL;
	const clientId = process.env.OAUTH_CLIENT_ID;
	const clientSecret = process.env.OAUTH_CLIENT_SECRET;

	if (!tokenUrl || !clientId || !clientSecret) {
		throw new Error('OAuth environment variables not configured');
	}

	const response = await fetch(tokenUrl, {
		method: 'POST',
		headers: {'Content-Type': 'application/x-www-form-urlencoded'},
		body: new URLSearchParams({
			grant_type: 'client_credentials',
			client_id: clientId,
			client_secret: clientSecret,
		}),
	});

	if (!response.ok) {
		throw new Error(`OAuth token request failed: ${response.status}`);
	}

	const data = await response.json();
	cachedToken = data.access_token;
	cachedAt = now;

	return cachedToken;
}

export default {
	id: 'oauth-token',
	name: 'OAuth Token Resolver',
	enable: true,

	async resolve(placeholders) {
		const result = {};

		if (placeholders.includes('oauth_token')) {
			try {
				result.oauth_token = await fetchOAuthToken();
			} catch (error) {
				// On failure, don't include the placeholder in the result.
				// The placeholder will be left as-is.
				// Snow CLI will log a warning.
				console.error('[oauth-token] Failed to fetch token:', error.message);
			}
		}

		return result;
	},
};
```

Configure custom headers:

| Key             | Value                    |
| --------------- | ------------------------ |
| `Authorization` | `Bearer {{oauth_token}}` |

## Multi-Plugin Files

You can register multiple plugins from a single file:

```js
export const customHeaderPlugins = [
	{
		id: 'timestamp',
		async resolve(placeholders) {
			const result = {};
			if (placeholders.includes('timestamp')) {
				result.timestamp = Date.now().toString();
			}
			return result;
		},
	},
	{
		id: 'uuid',
		async resolve(placeholders) {
			const result = {};
			if (placeholders.includes('uuid')) {
				result.uuid = crypto.randomUUID();
			}
			return result;
		},
	},
];
```

Convenient for plugins that share utility functions.

## Writing Your Own Plugin: Checklist

1. **Pick a stable, unique `id`**. Used for log identification; keep it unchanged
   once published.
2. **Only return successfully resolved placeholders**. Don't include
   placeholders you couldn't resolve — leave them for other plugins.
3. **Don't throw from `resolve()`**. If fetching a value fails, simply omit the
   placeholder from the result. Throwing is caught by Snow CLI and logged as a
   warning, but won't interrupt the request.
4. **Cache network requests**. `resolve()` is called on every API request. Without
   caching, every request triggers a network call.
5. **Don't rely on plugin order**. Plugins are loaded in filename order; don't
   assume your plugin runs before or after another.
6. **Use semantic placeholder names**. Use `{{oauth_token}}` instead of `{{t}}`
   to avoid collisions with other plugins.
7. **Leverage `context`**. `context.cwd`, `context.platform`, and
   `context.sessionId` can help you return different values based on the
   current environment or conversation session.
8. **Plugins run as Node.js modules**. You can `import` any Node.js built-in
   module (e.g. `crypto`, `fs`, `os`) and use `process.env` to read environment
   variables.

## Troubleshooting

- **Placeholders are not replaced and sent as-is.**

  - Make sure the plugin file is in `~/.snow/plugin/custom_headers/`.
  - Make sure the file extension is `.js` / `.mjs` / `.cjs`.
  - Check Snow CLI logs for `[custom-headers] failed to load plugin` or
    `Custom header plugin resolve failed`. Syntax errors and resolution
    exceptions are logged.
  - Make sure your export is a plain object with `{id, resolve}` — the loader
    logs `did not export a valid CustomHeaderPlugin` when validation fails.
  - Make sure the plugin doesn't have `enable: false`.

- **Plugin loaded but placeholders still not replaced.**

  - Check that `resolve()` returns an object containing the placeholder name as
    a key.
  - Make sure the returned value is a non-empty string — empty strings are
    ignored.
  - Make sure placeholder names match: `{{token}}` corresponds to a return key
    of `"token"` (whitespace is automatically trimmed).

- **I want to temporarily disable a plugin.**

  - Set `enable: false` in the plugin object. No need to delete the file. The
    plugin will not be called.

- **Will it affect anything if no plugins are installed?**

  - No. If header values don't contain `{{placeholders}}`, the plugin system is
    not involved at all and behavior is identical to before. Even if values
    contain placeholders but no plugins are installed, placeholders are sent
    as-is without errors.

## Related

- [Third-Party Relay Configuration](./16.Third-Party%20Relay%20Configuration.md) — basic custom headers configuration
- [Custom StatusLine Guide](./21.Custom%20StatusLine%20Guide.md) — same plugin-loading philosophy applied to the status line
- [Custom Search Engine Guide](./23.Custom%20Search%20Engine%20Guide.md) — same plugin-loading philosophy applied to search engines
